/**
 * 
 */
package neptune.manager.connector;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.UnknownHostException;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import neptune.manager.NeptuneManager;
import neptune.manager.model.topology.Node;
import neptune.manager.storage.ExperimentRepositoryException;
import neptune.manager.utils.DebugLog;
import neptune.manager.utils.SSHCommandException;
import neptune.manager.utils.SSHConnectionHandler;

import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * @author Roberto Bifulco
 * @author Luigi Lorenzo Arpino [luigiarpino@yahoo.it]
 * 
 */
public class LibvirtXenConnector implements Connector {

	public static final int DIST_UNDEFINED = -1;
	public static final int DIST_DEBIAN = 0;
	public static final int DIST_CENTOS = 1;
	public static final int DIST_FEDORA = 2;

	public static final String pathScriptSSH = System.getProperty("user.home")
			+ File.separator + ".emuPlugin/scripts/ssh.sh";
	public static final String pathScriptClone = System
			.getProperty("user.home")
			+ File.separator + ".emuPlugin/scripts/clone.sh";

	public static String getVMImgFilePath(String domainXMLDesc)
			throws SAXException, IOException, ParserConfigurationException {
		// Parsing stringa XML
		DocumentBuilderFactory docFactory = DocumentBuilderFactory
				.newInstance();
		docFactory.setValidating(false);

		InputStream inputStream = new ByteArrayInputStream(domainXMLDesc
				.getBytes());
		Document doc = docFactory.newDocumentBuilder().parse(inputStream);

		Element domainEl = doc.getDocumentElement();
		// Gets img file path
		NodeList diskEls = domainEl.getElementsByTagName("disk");
		Element diskEl = (Element) diskEls.item(0);
		NodeList nodeList = diskEl.getElementsByTagName("source");
		Element sourceEl = (Element) nodeList.item(0);

		return sourceEl.getAttribute("file");
	}

	public static void main(String[] args) throws ConnectorException {
		String ipAddr = "127.0.0.1";
		int port = 22;
		String pwd = "lorenzo";
		LibvirtXenConnector connector = new LibvirtXenConnector(ipAddr, port,
				pwd);
		connector.TerminateVM("template2");
	}

	private Connect connection;
	private String ipAddr;

	private String pwd;

	private final int port;

	public LibvirtXenConnector(String ipAddr, int port, String pwd)
			throws ConnectorException {
		super();
		this.ipAddr = ipAddr;
		this.port = port;
		this.pwd = pwd;
		try {
			// controllo la configurazione
			this.checkConfiguration(ipAddr, port, "root", pwd);

			// no_tty garantisce che in caso di necessita' di interattivita',
			// semplicemente l'auth fallisca
			this.connection = new Connect("xen+ssh://root@" + ipAddr + ":"
					+ port + "/?no_tty=1&command=" + pathScriptSSH, false);
		} catch (LibvirtException e) {
			DebugLog.printDetailedLog(e.getMessage(), LibvirtXenConnector.class
					.getSimpleName());
			throw new ConnectorException(e);
		} catch (FileNotFoundException e) {
			DebugLog.printDetailedLog(e.getMessage(),
					FileNotFoundException.class.getSimpleName());
			throw new ConnectorException(e);
		} catch (IOException e) {
			DebugLog.printDetailedLog(e.getMessage(), IOException.class
					.getSimpleName());
			throw new ConnectorException(e);
		} catch (Exception e) {
			DebugLog.printDetailedLog(e.getMessage(), IOException.class
					.getSimpleName());
			throw new ConnectorException(e);
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see neptune.manager.connector.Connector#cloneVM(java.lang.String)
	 */
	@Override
	public void cloneVM(String vmId, String destinationPath)
			throws ConnectorException {
		try {
			Domain domain = connection.domainLookupByName(vmId);

			String xml = domain.getXMLDesc(0);
			String imgFilePath = getVMImgFilePath(xml);

			swapOutVM(vmId);

			// TODO eliminare la dipendenza dal singleton NeptuneManager
			String expId = NeptuneManager.getExpNameFromNodeFullID(vmId);
			String nodeId = NeptuneManager.getNodeNameFromNodeFullID(vmId);
			Node node = NeptuneManager.instance().getExperiment(expId)
					.getTopology().searchNodeIntoTopology(nodeId);

			copyImgFile(imgFilePath, destinationPath);

			String[] macAddresses = new String[node.getInterfaces().length];
			for (int i = 0; i < macAddresses.length; i++)
				macAddresses[i] = node.getInterfaces()[i].getMacAddress()
						.getAddress();

			swapInVM(vmId, imgFilePath, node.getRam(), macAddresses, null);

		} catch (LibvirtException e) {
			throw new ConnectorException(e);
		} catch (IOException e) {
			throw new ConnectorException(e);
		} catch (ParserConfigurationException e) {
			throw new ConnectorException(e);
		} catch (SAXException e) {
			throw new ConnectorException(e);
		} catch (ExperimentRepositoryException e) {
			throw new ConnectorException(e);
		}
	}

	@Override
	public void closeConnection() throws ConnectorException {
		try {
			connection.close();
			DebugLog.printDetailedLog("Connection closed",
					LibvirtXenConnector.class.getSimpleName());
		} catch (LibvirtException e) {
			DebugLog
					.printDetailedLog("Connection closing exception: "
							+ e.getMessage(), LibvirtXenConnector.class
							.getSimpleName());
			throw new ConnectorException(e);
		}

	}

	@Override
	public void createVM(String vmId, String ipAddress, String netMask,
			String gateway, String[] macAddresses, String sourceTemplatePath,
			String destTemplatePath, int distributionType, int ramDim,
			String cloneScriptPath, String losetupPath, String kpartxPath,
			String bridge) throws ConnectorException {

		SSHConnectionHandler handler = null;

		/*
		 * Creazione della copia dell'immagine del nodo, tramite esecuzione di
		 * clone.sh
		 */
		try {
			handler = SSHConnectionHandler.createSSHConnectionHandler(ipAddr,
					port, "root", pwd);

			executeCloneScript(handler, cloneScriptPath, losetupPath,
					kpartxPath, distributionType, sourceTemplatePath,
					destTemplatePath, ipAddress, netMask, gateway,
					macAddresses[0]);

			handler.dispose();
		} catch (UnknownHostException e) {
			DebugLog.printDetailedLog(e.getMessage(), LibvirtXenConnector.class
					.getSimpleName());
			throw new ConnectorException(e);
		} catch (IOException e) {
			DebugLog.printDetailedLog(e.getMessage(), LibvirtXenConnector.class
					.getSimpleName());
			throw new ConnectorException(e);
		} catch (SSHCommandException e) {
			DebugLog.printDetailedLog(e.getMessage(), LibvirtXenConnector.class
					.getSimpleName());

			BufferedReader reader = handler.getStdOut();

			String msgScript;
			try {
				while ((msgScript = reader.readLine()) != null) {
					if (msgScript.contains("ERROR")) {
						e.printStackTrace();
						throw new ConnectorException(msgScript);
					}
				}
			} catch (IOException e1) {
				DebugLog.printDetailedLog(e1.getMessage(),
						LibvirtXenConnector.class.getSimpleName());
				throw new ConnectorException(e);
			}
		}

		/*
		 * Creazione del dominio sull'hypervisor TODO inserire una semplice
		 * chiamata a swapInVM in sostituzione
		 */
		String domainXMLDescr = getDomainXMLDescription(vmId, ramDim,
				destTemplatePath);

		DebugLog.printDetailedLog("Domain XML description:\n" + domainXMLDescr,
				LibvirtXenConnector.class.getSimpleName());
		DebugLog.printDetailedLog("Creating domain", LibvirtXenConnector.class
				.getSimpleName());

		try {
			connection.domainCreateLinux(domainXMLDescr, 0);

			DebugLog.printDetailedLog("Domain created",
					LibvirtXenConnector.class.getSimpleName());

			Domain d = connection.domainLookupByName(vmId);

			// Definisco le interfaccie di rete
			for (String macAddr : macAddresses) {
				String deviceXML = getInterfaceXMLDescription(macAddr, bridge);
				DebugLog.printDetailedLog("Attaching network interface:\n"
						+ deviceXML, LibvirtXenConnector.class.getSimpleName());
				d.attachDevice(deviceXML);
			}
		} catch (LibvirtException e) {
			DebugLog.printDetailedLog(e.getMessage(), LibvirtXenConnector.class
					.getSimpleName());
			throw new ConnectorException(e);
		}
	}

	@Override
	public String[][] getAllDomains() throws ConnectorException {
		try {
			int[] idDomini = connection.listDomains();
			String[] nomiDomini = new String[idDomini.length];
			String[] statoDomini = new String[idDomini.length];
			String[] memDomini = new String[idDomini.length];
			for (int i = 0; i < idDomini.length; i++) {
				Domain d = connection.domainLookupByID(idDomini[i]);
				nomiDomini[i] = d.getName();
				String stato = d.getInfo().state.toString();
				StringTokenizer st = new StringTokenizer(stato, "_");
				st.nextToken();
				st.nextToken();
				statoDomini[i] = st.nextToken();
				memDomini[i] = String.valueOf(d.getInfo().memory);
			}
			String[][] result = new String[idDomini.length][4];
			for (int i = 0; i < idDomini.length; i++) {
				result[i][0] = String.valueOf(idDomini[i]);
				result[i][1] = nomiDomini[i];
				result[i][2] = statoDomini[i];
				result[i][3] = memDomini[i];
			}
			return result;
		} catch (LibvirtException e) {
			throw new ConnectorException(e);
		}
	}

	@Override
	public String[] getPhysicalMachineStatus() throws ConnectorException {
		// TODO DA IMPLEMENTARE
		String[] result = new String[3];
		result[0] = "4";
		result[1] = "1024";
		result[2] = "160";
		return result;
	}

	/**
	 * Restituisce informazioni circa il dominio identificato da vmId
	 * 
	 * La 1° colonna contiene l'id del dominio la 2° colonna contiene il nomi
	 * del dominio la 3° colonna contiene lo stato* del dominio la 4° colonna
	 * contiene la memoria ram occupata
	 * 
	 * *stato={NOSTATE, RUNNING, BLOCKED, PAUSED, SHUTDOWN, SHUTOFF, CRASHED}
	 */
	@Override
	public String[] getVMStatus(String vmId) throws ConnectorException {
		String[] result = new String[4];
		try {
			Domain d = connection.domainLookupByName(vmId);
			String id = String.valueOf(d.getID());
			String stato = d.getInfo().state.toString();
			StringTokenizer st = new StringTokenizer(stato, "_");
			st.nextToken();
			st.nextToken();
			String statoDominio = st.nextToken();
			String memoria = String.valueOf(d.getInfo().memory);
			result[0] = id;
			result[1] = vmId;
			result[2] = statoDominio;
			result[3] = memoria;
		} catch (LibvirtException e) {
			throw new ConnectorException(e);
		}
		return result;
	}

	@Override
	public void pauseVM(String vmId) throws ConnectorException {
		try {
			Domain d = connection.domainLookupByName(vmId);
			d.suspend();
			DebugLog.printDetailedLog("Domain" + vmId + " paused",
					LibvirtXenConnector.class.getSimpleName());
		} catch (LibvirtException e) {
			throw new ConnectorException(e);
		}

	}

	@Override
	public void rebootVM(String vmId) throws ConnectorException {
		try {
			Domain d = connection.domainLookupByName(vmId);
			d.reboot(0);
			DebugLog.printDetailedLog("Domain" + vmId + " rebooted",
					LibvirtXenConnector.class.getSimpleName());
		} catch (LibvirtException e) {
			throw new ConnectorException(e);
		}
	}

	/**
	 * Rende attivo un dominio che era stato messo in pausa
	 */
	@Override
	public void startVM(String vmId) throws ConnectorException {
		try {
			Domain d = connection.domainLookupByName(vmId);
			d.resume();
			DebugLog.printDetailedLog("Domain" + vmId + " started",
					LibvirtXenConnector.class.getSimpleName());
		} catch (LibvirtException e) {
			throw new ConnectorException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see neptune.manager.connector.Connector#swapInVM(java.lang.String)
	 */
	@Override
	public void swapInVM(String vmId, String imgFilePath, int ramDim,
			String[] macAddresses, String bridge) throws ConnectorException {

		/*
		 * Creazione del dominio sull'hypervisor
		 */
		String domainXMLDescr = getDomainXMLDescription(vmId, ramDim,
				imgFilePath);

		DebugLog.printDetailedLog("Domain XML description:\n" + domainXMLDescr,
				LibvirtXenConnector.class.getSimpleName());
		DebugLog.printDetailedLog("Creating domain", LibvirtXenConnector.class
				.getSimpleName());

		try {
			connection.domainCreateLinux(domainXMLDescr, 0);

			DebugLog.printDetailedLog("Domain created",
					LibvirtXenConnector.class.getSimpleName());

			Domain d = connection.domainLookupByName(vmId);

			// Definisco le interfaccie di rete
			for (String macAddr : macAddresses) {
				String deviceXML = getInterfaceXMLDescription(macAddr, bridge);
				DebugLog.printDetailedLog("Attaching network interface:\n"
						+ deviceXML, LibvirtXenConnector.class.getSimpleName());
				d.attachDevice(deviceXML);
			}
		} catch (LibvirtException e) {
			DebugLog.printDetailedLog(e.getMessage(), LibvirtXenConnector.class
					.getSimpleName());
			throw new ConnectorException(e);
		}

	}

	@Override
	public void swapOutVM(String vmId) throws ConnectorException {
		System.out.println("swapOutVM " + vmId);
		try {
			Domain d = connection.domainLookupByName(vmId);
			d.destroy();
			DebugLog.printDetailedLog("Domain" + vmId + " destroyed",
					LibvirtXenConnector.class.getSimpleName());
		} catch (LibvirtException e) {
			throw new ConnectorException(e);
		}
	}

	@Override
	public void TerminateVM(String vmId) throws ConnectorException {
		System.out.println("TerminateVM " + vmId);
		try {
			Domain domain = connection.domainLookupByName(vmId);

			// Gets domain xml description to retrieve the img file path
			String xml = domain.getXMLDesc(0);
			String pathFile = getVMImgFilePath(xml);

			DebugLog.printDetailedLog("Domain" + vmId + " try to destry...",
					LibvirtXenConnector.class.getSimpleName());
			domain.destroy();
			DebugLog.printDetailedLog("Domain" + vmId + " is  destroyed now!",
					LibvirtXenConnector.class.getSimpleName());

			if (pathFile != null && !pathFile.equalsIgnoreCase("")) {
				DebugLog.printDetailedLog("Try to delete file " + pathFile
						+ " ...", LibvirtXenConnector.class.getSimpleName());
				File file = new File(pathFile);
				file.delete();
				DebugLog.printDetailedLog("File " + pathFile
						+ " is deleted now!", LibvirtXenConnector.class
						.getSimpleName());
			}
		} catch (LibvirtException e) {
			throw new ConnectorException(e);
		} catch (SAXException e) {
			throw new ConnectorException(e);
		} catch (IOException e) {
			throw new ConnectorException(e);
		} catch (ParserConfigurationException e) {
			throw new ConnectorException(e);
		}
	}

	/**
	 * Controlla la corretta configurazione del file di configurazione ssh
	 * 
	 * @throws ConnectorException
	 * @throws IOException
	 * @throws SSHCommandException
	 */
	private void checkConfiguration(String ipAddr, int port, String user,
			String pwd) throws ConnectorException, IOException,
			SSHCommandException {
		// testo la connettività del server
		SSHConnectionHandler handler = SSHConnectionHandler
				.createSSHConnectionHandler(ipAddr, port, user, pwd);
		// Verifico se la chiave è già presente sul server
		handler.sendCommand("cat /root/.ssh/authorized_keys | grep \""
				+ System.getProperty("user.name") + "\" | wc -l");
		BufferedReader reader = handler.getStdOut();
		int numChiave = Integer.valueOf(reader.readLine().trim());
		if (numChiave == 0) {
			String msg = "\n The server "
					+ ipAddr
					+ " may not have public key for user "
					+ System.getProperty("user.name")
					+ "."
					+ "\n Please check file /root/.ssh/authorized_keys on server "
					+ ipAddr;
			throw new ConnectorException(msg);
		}
		handler.dispose();
		// testo la presenza dei file richiesti
		// String fileSeparator=System.getProperty("file.separator");
		// String
		// fileConfigSSH=System.getProperty("user.home")+fileSeparator+".ssh"+fileSeparator+"config";
		// verifico la presenza di /usr/lib/libvirt_jni.so
		String pathJNIfile = "/usr/lib/libvirt_jni.so";
		File file = new File(pathJNIfile);
		if (!file.exists())
			throw new ConnectorException("\n\n File " + pathJNIfile
					+ " not Found! ");
		// verifico la presenza di dello script ssh.sh
		File file2 = new File(pathScriptSSH);
		if (!file2.exists())
			throw new ConnectorException("\n\n File " + pathScriptSSH
					+ " not Found! ");
		if (!file2.canExecute())
			throw new ConnectorException("\n\n Unable to execute "
					+ pathScriptSSH + " Please check permission!");
	}

	private void executeCloneScript(SSHConnectionHandler handler,
			String cloneScriptPath, String losetupPath, String kpartxPath,
			int distributionType, String sourceTemplatePath,
			String destTemplatePath, String ipAddress, String netMasks,
			String gateway, String macAddress) throws SSHCommandException,
			IOException, ConnectorException {
		handler.sendCommand("rm -f /tmp/clone.sh");

		// copio lo script sul worker node
		/*
		 * BufferedReader fin = new BufferedReader(new
		 * FileReader(cloneScriptPath));
		 * 
		 * String riga; while ((riga = fin.readLine()) != null) { riga =
		 * riga.replace("LOSETUP_PATH=/sbin", "LOSETUP_PATH=" + losetupPath);
		 * riga = riga.replace("KPARTX_PATH=/sbin", "KPARTX_PATH=" +
		 * kpartxPath); handler.sendCommand("echo '" + riga +
		 * "' >> /tmp/clone.sh"); }
		 */
		// copio lo script sul worker node
		String command = "scp " + pathScriptClone + " root@" + this.ipAddr
				+ ":/tmp/clone.sh";
		System.out.println("command " + command);
		Process proc = Runtime.getRuntime().exec(command);
		try {
			proc.waitFor();
		} catch (Exception e) {

		}

		handler.sendCommand("chmod 700 /tmp/clone.sh");
		// fin.close();
		// controllo la presenza dello script sul worker node
		handler.sendCommand("ls /tmp |grep \"clone.sh\"|wc -l");
		BufferedReader reader = handler.getStdOut();
		int numChiave = Integer.valueOf(reader.readLine().trim());
		if (numChiave != 1) {
			String msg = "\n ERROR Unable to copy script clone.sh on server "
					+ ipAddr;
			throw new SSHCommandException(msg);
		}

		switch (distributionType) {
		case LibvirtXenConnector.DIST_DEBIAN:
			handler.sendCommand("/tmp/clone.sh debian " + sourceTemplatePath
					+ " " + destTemplatePath + " " + ipAddress + " " + netMasks
					+ " " + gateway + " eth0 " + macAddress);
			break;
		case LibvirtXenConnector.DIST_CENTOS:
			handler.sendCommand("/tmp/clone.sh centos " + sourceTemplatePath
					+ " " + destTemplatePath + " " + ipAddress + " " + netMasks
					+ " " + gateway + " eth0 " + macAddress);
			break;
		case LibvirtXenConnector.DIST_FEDORA:
			handler.sendCommand("/tmp/clone.sh fedora " + sourceTemplatePath
					+ " " + destTemplatePath + " " + ipAddress + " " + netMasks
					+ " " + gateway + " eth0 " + macAddress);
			break;
		default:
			throw new ConnectorException(
					"Error script clone.sh. Unable to clone operative system! Please check xml descriptor of Virtual Machine.");
		}

		reader = handler.getStdOut();

		String msgScript;
		while ((msgScript = reader.readLine()) != null) {
			DebugLog.printDetailedLog(msgScript, LibvirtXenConnector.class
					.getSimpleName());
		}

	}

	/**
	 * Crea la descrizione xml necessaria a libvirt per avviare il nodo.
	 * Attualmente sono pochi i parametri configurabili in questa descrizione.
	 * Da notare, in particolare, il driver adoperato per il disco della
	 * macchina virtuale, che utilizza un file, con driver 'tap:aio'. Il device
	 * target per il disco e' anch'esso fisso ed e' 'xvda'
	 * 
	 * 
	 * @param vmId
	 * @param ramDim
	 * @param destTemplatePath
	 * @return
	 */
	private String getDomainXMLDescription(String vmId, int ramDim,
			String destTemplatePath) {

		String xml = "";

		// Domain type >>>>>
		xml += "<domain type='xen'>";
		// Domain name
		xml += "<name>" + vmId + "</name>";
		// Bootloader
		xml += "<bootloader>/usr/bin/pygrub</bootloader>";
		// os definition
		xml += "<os><type>linux</type></os>";
		// ram dimension
		xml += "<memory>" + ramDim * 1024 + "</memory>";
		// vcpu
		xml += "<vcpu>1</vcpu>";
		// lifecycle control
		xml += "<on_poweroff>destroy</on_poweroff>";
		xml += "<on_reboot>restart</on_reboot>";
		xml += "<on_crash>destroy</on_crash>";
		// Devices >>>>>
		xml += "<devices>";
		// disk >>>>>
		xml += "<disk type='file' device='disk'>";
		xml += "<driver name='tap' type='aio'/>";
		xml += "<source file='" + destTemplatePath + "'/>";
		xml += "<target dev='xvda'/>";
		// disk <<<<<
		xml += "</disk>";
		// console
		xml += "<console tty='/dev/pts/2'/>";
		// Devices <<<<<
		xml += "</devices>";
		// Domain type <<<<<
		xml += "</domain>";

		return xml;
	}

	private String getInterfaceXMLDescription(String macAddress, String bridge) {
		String deviceXML = "";

		deviceXML += "<interface type='bridge'>";
		deviceXML += "<mac address='" + macAddress + "'/>";
		if (bridge != null && !bridge.equals(""))
			deviceXML += "<source bridge='" + bridge + "'/>";
		deviceXML += "</interface>";

		return deviceXML;
	}

	private void copyImgFile(String sourcePath, String destPath)
			throws IOException {
		File source = new File(sourcePath);
		File dest = new File(destPath);
		dest.createNewFile();

		FileInputStream fis = new FileInputStream(source);
		FileOutputStream fos = new FileOutputStream(dest);

		byte[] buf = new byte[4096];
		int i = 0;
		while ((i = fis.read(buf)) != -1)
			fos.write(buf, 0, i);
	}
}
